﻿///
/// NetlistParse.fs
/// Defines parsing elements.
///

namespace PathFinder

open System
open System.IO
open System.Collections
open System.Collections.Generic
open System.Text.RegularExpressions
open Microsoft.FSharp

open PathFinder.Network

#light

///
/// Contains the most important elements for parsing the netlist.
///
module public NetlistParse =
    (* Types and constants *)
    ///
    /// Path to the netlist file.
    ///
    type public NetFilePath = string
    ///
    /// Default name for the netfile.
    ///
    [<Literal>]
    let public DefaultNetFIleName: NetFilePath = "netfile.txt"
    ///
    /// Regular expression for parsing node identifiers.
    ///
    [<Literal>]
    let public NodeIdentifierRegex = "\<([a-zA-Z0-9])+([a-zA-Z0-9\s])*([a-zA-Z0-9])+\>"
    ///
    /// Regular expression for parsing coordinates.
    ///
    [<Literal>]
    let public NodeCoordinatesRegex = "\(([0-9])+\.([0-9])+\s([0-9])+\.([0-9])+\)"
    ///
    /// Regular expression for parsing weight values.
    ///
    [<Literal>]
    let public WeightValueRegex = "\"([0-9])+(.){0,1}([0-9])+\""
    ///
    /// Regular expression for parsing finite comments.
    ///
    [<Literal>]
    let public FiniteCommentRegex = "/\*([0-9a-zA-Z\s])*\*/"
    ///
    /// Regular expression for parsing space delimiters. This maps a compulsory space (that can include more spaces).
    ///
    [<Literal>]
    let public SpacesRegex = "(\s)+"
    ///
    /// Regular expression for parsing possible space delimiters. This space can be optional.
    ///
    [<Literal>]
    let public PossibleSpacesRegex = "(\s)*"
    ///
    /// Regular expression for parsing declaration delimiter.
    ///
    [<Literal>]
    let public SeparatorChar = ';'
    ///
    /// Regular expression for parsing node declarations. Separator used to split elements in line.
    ///
    let public NodeDeclarationRegex = PossibleSpacesRegex + "node" + SpacesRegex + NodeIdentifierRegex
    ///
    /// Regular expression for parsing full node declarations. Separator used to split elements in line.
    ///
    let public NodeFullDeclarationRegex = NodeDeclarationRegex + SpacesRegex + "at" + SpacesRegex + NodeCoordinatesRegex
    ///
    /// Regular expression for parsing connection declarations. Separator used to split elements in line.
    ///
    let public ConnectionDeclarationRegex = PossibleSpacesRegex + "connection" + SpacesRegex + "from" + SpacesRegex + NodeIdentifierRegex + SpacesRegex + "to" + SpacesRegex + NodeIdentifierRegex + SpacesRegex + "with" + SpacesRegex + WeightValueRegex
    ///
    /// Regular expression for parsing queries declarations. Separator used to split elements in line.
    ///
    let public QueryDeclarationRegex = PossibleSpacesRegex + "pathquery" + SpacesRegex + "from" + SpacesRegex + NodeIdentifierRegex + SpacesRegex + "to" + SpacesRegex + NodeIdentifierRegex
    ///
    /// Regular expression for comments.
    ///
    let public CommentRegex = "//([a-zA-Z0-9 ])*"
    ///
    /// Type for a recognized node.
    ///
    type public RecognizedNode = {
        NodeId: NodeIdentifier;
        NodeCoordinates: NodeCoordinate
        }
    ///
    /// Type for recognized connection.
    ///
    type public RecognizedConnection = {
        NodeFromId: NodeIdentifier;
        NodeToId: NodeIdentifier;
        ConnectionCost: ConnectionWeight
        }
    ///
    /// Type for recognized queries.
    ///
    type public RecognizedPathQuery = {
        PathStartNode: NodeIdentifier;
        PathEndNode: NodeIdentifier
        }
    ///
    /// Type for an error.
    ///
    type public FileError = {
        LineNumber: int;
        TokenNumber: int;
        Expression: string;
        Error: string
        }
    ///
    /// Type for the log of the parser.
    ///
    type public ParserLogEntry = {
        LineNumber: int;
        TokenNumber: int;
        Line: string;
        Token: string;
        Log: string
        }
    ///
    /// Record for results of file check.
    ///
    type public CheckFileResult = {
        Correctness: bool;
        Log: ParserLogEntry list;
        Errors: FileError list
        }
    ///
    /// Record for results of the entire process of parsing.
    ///
    type public ParseFileResults = {
        CheckResult: CheckFileResult;
        Nodes: RecognizedNode list;
        Connections: RecognizedConnection list;
        PathQueries: RecognizedPathQuery list
        }
    ///
    /// Exception when the netfile could not parsed correctly.
    ///
    exception public FileParseFailed of string
    ///
    /// Exception when a connection uses nodes not defined.
    ///
    exception public UndefinedNodeUsage of string
    (* Functions *)
    ///
    /// Prints a single File error record. Gets the string.
    ///
    let public PrintFileError (fe: FileError) =
        "Error at Line " + (fe.LineNumber.ToString ()) + " on Token " + (fe.TokenNumber.ToString ()) + ": TOKEN is \"" + fe.Expression + "\". " + fe.Error
    ///
    /// Prints a single log record. Gets the string.
    ///
    let public PrintParserLogEntry (ple: ParserLogEntry) =
        "Parsing Line " + (ple.LineNumber.ToString ()) + " Token " + (ple.TokenNumber.ToString ()) + ": LINE is \"" + ple.Line + "\" ==> TOKEN is \"" + ple.Token + "\". " + ple.Log + "."
    ///
    /// Takes the file and returns the collection of strings representing it.
    ///
    let public GetFileLines (filepath: NetFilePath) =
        File.ReadAllLines(filepath)
    ///
    /// Used to eliminate finite comment in a string. Everything inside /* and */ is deleted from the string.
    ///
    let private RemoveFiniteComments (s: string) =
        true
    ///
    /// Parses the file exctrancting the desired information. This function returns two lists: a list of nodes and a list of connections (recognized nodes and connections).
    /// Line: a line in the file.
    /// Token: String in a line starting from the beginning of the line until the separator character, or the string from a separator character to the next in the line.
    /// Possible recognized tokens: Comments, Node declarations and Connection declarations.
    ///
    let public ParseNetFile (filepath: NetFilePath) = 
        (* Defining output quantities *)
        let mutable recgNodes: RecognizedNode list = [] (* Out *)
        let mutable recgConnections: RecognizedConnection list = [] (* Out *)
        let mutable recgPathQueries: RecognizedPathQuery list = [] (* Out *)
        let mutable correct = true
        (* Defining tracing quantities *)
        let mutable logs: ParserLogEntry list = [] (* FOR LOGGING *)
        let mutable errs: FileError list = [] (* FOR ERRORS *)
        let filelines = GetFileLines (filepath) (* Getting all lines of the file as a collection *)
        (* Indexes for line and token in line *)
        let mutable line_index = 0 (* Index for line *)
        let mutable token_index = 0 (* Index for token in line *)
        (* Cycling for evey line *)
        for i in filelines do
            line_index <- (line_index + 1) (* Updating line index *)
            token_index <- 0 (* Resetting token index *)
            let subs = i.Split ([|SeparatorChar|], System.StringSplitOptions.RemoveEmptyEntries) (* Splitting for separator in a line and getting tokens *)
            let empty = (i = "")
            (* Cycling for evey token *)
            for k in subs do
                (* Removing token finite comments *)
                let j = (new Regex (FiniteCommentRegex)).Replace (k, "")
                token_index <- (token_index + 1) (* Updating token index *)
                (* Defining regexes for testing possible tokens: a node, a connection or a comment *)
                let rxnode = new Regex (NodeDeclarationRegex) (* Regex for checking the presence of a node declaration token *)
                let rxfnode = new Regex (NodeFullDeclarationRegex) (* Regex for checking the presence of a full node declaration token *)
                let rxconn = new Regex (ConnectionDeclarationRegex) (* Regex for checking the presence of a connection declaration token *)
                let rxcomm = new Regex (CommentRegex) (* Regex for checking the presence of a comment token *)
                let rxptqr = new Regex (QueryDeclarationRegex) (* Regex for checking the presence of a query declaration token *)
                let rxblnk = new Regex (PossibleSpacesRegex) (* Maybe the user entered just spaces *)
                (* Defining regexes for getting specific information in a token *)
                let rxnodeid = new Regex (NodeIdentifierRegex) (* Regex for getting the node identifier <NodeId> in a node/connection declaration token *)
                let rxweight = new Regex (WeightValueRegex) (* Regex for getting the weight value "Weight" in a connection declaration token *)
                (* Testing the token: is it a comment, a connection declaration or a node declaration? *)
                let isnode = (((rxnode.Matches (j)).Count) >= 1) (* Testing for node declaration *)
                let isfnode = (((rxfnode.Matches (j)).Count) >= 1) (* Testing for fullnode declaration *)
                let isconn = (((rxconn.Matches (j)).Count) >= 1) (* Testing for connection declaration *)
                let iscomm = (((rxcomm.Matches (j)).Count) >= 1) (* Testing for commment declaration *)
                let isptqr = (((rxptqr.Matches (j)).Count) >= 1) (* Testing for query declaration *)
                let isblnk = (((rxblnk.Matches (j)).Count) >= 1) (* Testing for blank spaces to ignore *)
                (* Finally get the type of token, here we tell for sure what we are handling *)
                match (isnode, isfnode, isconn, iscomm, isptqr, isblnk) with
                    | (true, false, false, _, false, _) -> 
                        (* Parsing a Node *)
                        logs <- logs @ [{LineNumber = line_index; TokenNumber = token_index; Line = i; Token = j; Log = "Node recognized"}] (* Updating log *)
                        let nodeid = (((((new Regex (NodeIdentifierRegex)).Match (j)).Value).Replace ("<", "")).Replace (">", "")) (* Getting node id *)
                        recgNodes <- recgNodes @ [{NodeId = nodeid; NodeCoordinates = (0.0, 0.0)}] (* Adding to recognized nodes *)
                    | (_, true, false, _, false, _) -> 
                        (* Parsing a Full Node *)
                        logs <- logs @ [{LineNumber = line_index; TokenNumber = token_index; Line = i; Token = j; Log = "Node recognized"}] (* Updating log *)
                        let nodeid = (((((new Regex (NodeIdentifierRegex)).Match (j)).Value).Replace ("<", "")).Replace (">", "")) (* Getting node id *)
                        let coord1 = double (((((((new Regex (NodeCoordinatesRegex)).Match (j)).Value).Replace ("(", "")).Replace (")", ""))).Split ([|' '|])).[0] (* Getting coordinate 1 *)
                        let coord2 = double (((((((new Regex (NodeCoordinatesRegex)).Match (j)).Value).Replace ("(", "")).Replace (")", ""))).Split ([|' '|])).[1] (* Getting coordinate 2 *)
                        recgNodes <- recgNodes @ [{NodeId = nodeid; NodeCoordinates = (coord1, coord2)}] (* Adding to recognized nodes *)
                    | (false, false, true, _, false, _) -> 
                        (* Parsing a Connection *)
                        logs <- logs @ [{LineNumber = line_index; TokenNumber = token_index; Line = i; Token = j; Log = "Connection recognized"}] (* Updating log *)
                        let nodefromid = (((((new Regex (NodeIdentifierRegex)).Matches (j)).[0].Value).Replace ("<", "")).Replace (">", "")) (* Getting first node id *)
                        let nodetoid = (((((new Regex (NodeIdentifierRegex)).Matches (j)).[1].Value).Replace ("<", "")).Replace (">", "")) (* Getting the other node id *)
                        let cost = ((((new Regex (WeightValueRegex)).Match (j)).Value).Replace ("\"", "")) (* Getting the cost *)
                        recgConnections <- recgConnections @ [{NodeFromId = nodefromid; NodeToId = nodetoid; ConnectionCost = double cost}] (* Adding to recognized connections *)
                    | (false, false, false, true, false, _) -> 
                        (* Parsing a Comment --> Do nothing, just log! *)
                        logs <- logs @ [{LineNumber = line_index; TokenNumber = token_index; Line = i; Token = j; Log = "Comment recognized"}] (* Updating log *)
                    | (false, false, false, _, true, _) -> 
                        (* Parsing a query --> Register it! *)
                        logs <- logs @ [{LineNumber = line_index; TokenNumber = token_index; Line = i; Token = j; Log = "Path query recognized"}] (* Updating log *)
                        let nodestart = (((((new Regex (NodeIdentifierRegex)).Matches (j)).[0].Value).Replace ("<", "")).Replace (">", "")) (* Getting first node id *)
                        let nodeend = (((((new Regex (NodeIdentifierRegex)).Matches (j)).[1].Value).Replace ("<", "")).Replace (">", "")) (* Getting other node id *)
                        recgPathQueries <- recgPathQueries @ [{PathStartNode = nodestart; PathEndNode = nodeend}]
                    | (false, false, false, false, false, true) ->
                        (* Parsing a blank --> Do nothing, just log! *)
                        logs <- logs @ [{LineNumber = line_index; TokenNumber = token_index; Line = i; Token = j; Log = "Blank spaces recognized"}] (* Updating log *)
                    | _ -> 
                        (* Parsing Unrecognized --> Error! *)
                        errs <- errs @ [{LineNumber = line_index; TokenNumber = token_index; Expression = j; Error = "Unrecognized expression"}] (* Updating errors *)
                        correct <- false (* Fail in parsing *)
        (* Getting final result *)
        let check = {Correctness = correct; Log = logs; Errors = errs} (* Building the check result *)
        {CheckResult = check; Nodes = recgNodes; Connections = recgConnections; PathQueries = recgPathQueries} (* Building and getting the final result *)
    ///
    /// Interpretes the semantics and gets the connections that uses nodes that are not defined.
    /// Returns empty list when no error occurs, a list filled with RecognizedConnections otherwise.
    ///
    let rec public AnalyzeConnectionsAfterParse (nodes: RecognizedNode list) (connections: RecognizedConnection list) (unrecognized_connections: RecognizedConnection list) =
        match connections with
        | [] ->
            (* Exit case *)
            unrecognized_connections
        | h :: conn_tail ->
            (* Continue -> h is the element *)
            let node_from_exists = List.exists (fun x -> x.NodeId = h.NodeFromId) nodes
            let node_to_exists = List.exists (fun x -> x.NodeId = h.NodeToId) nodes
            if (node_from_exists && node_to_exists) then
                (* Nodes both exist *)
                AnalyzeConnectionsAfterParse nodes conn_tail unrecognized_connections
            else
                (* At least a node does not exist *)
                AnalyzeConnectionsAfterParse (nodes) (conn_tail) (h :: unrecognized_connections)
    ///
    /// Interpretes the semantics and gets the queries that uses nodes that are not defined.
    /// Returns empty list when no error occurs, a list filled with RecognizedConnections otherwise.
    ///
    let rec public AnalyzePathQueriesAfterParse (nodes: RecognizedNode list) (queries: RecognizedPathQuery list) (unrecognized_queries: RecognizedPathQuery list) =
        match queries with
        | [] ->
            (* Exit case *)
            unrecognized_queries
        | h :: queries_tail ->
            (* Continue -> h is the element *)
            let node_from_exists = List.exists (fun x -> x.NodeId = h.PathStartNode) nodes
            let node_to_exists = List.exists (fun x -> x.NodeId = h.PathEndNode) nodes
            if (node_from_exists && node_to_exists) then
                (* Nodes both exist *)
                AnalyzePathQueriesAfterParse nodes queries_tail unrecognized_queries
            else
                (* At least a node does not exist *)
                AnalyzePathQueriesAfterParse (nodes) (queries_tail) (h :: unrecognized_queries)
